type TickOptions = {
  isShow: boolean;
  color?: string;
  gradual?: boolean;
  tickLength?: number;
  splitTickLength?: number;
  tickFloat?: number;
  splitTickFloat?: number;
  tickNum?: number;
  splitTickNum?: number;
};

type TextStyle = {
  color?: string;
  fontSize?: number;
  fontWeight?: string;
  marginTop?: number;
  textShadowColor?: string;
  shadowOffsetX?: number;
  shadowOffsetY?: number;
  textShadowBlur?: number;
};

type ITextOptions = {
  isShow: boolean;
  offsetY: number;
  text: string[];
  styles?: TextStyle[];
};

export type IOptions = {
  min?: number;
  max?: number;
  value?: number;
  speed?: number;
  radius?: number;
  lineWidth?: number;
  roundCap?: boolean;
  progressBgImage?: string;
  startAngle?: number;
  endAngle?: number;
  progressBgColor?: string;
  progressColor?: string;

  tick?: TickOptions;
  text?: ITextOptions;
};

/**
 * 仪表盘类
 */
export default class Dashboard {
  container: HTMLElement;
  private _w: number;
  private _h: number;
  private _r: number;

  min: number;
  max: number;
  value: number;
  speed: number;
  radius: number;
  lineWidth: number;
  roundCap: boolean;
  progressBgImage: string;
  startAngle: number;
  endAngle: number;
  progressBgColor: string;
  progressColor: string;

  _tick: TickOptions | null;
  _text: ITextOptions | null;

  _canvasElement: HTMLCanvasElement;
  _ctx: CanvasRenderingContext2D;
  _bgPattern: CanvasPattern | null;
  _imageBgElementCache: HTMLImageElement | null;

  constructor(container: HTMLElement) {
    this.container = container;
    this._w = parseFloat(getStyle(container, "width").replace("px", ""));
    this._h = parseFloat(getStyle(container, "height").replace("px", ""));
    this._r = Math.min(this._w, this._h) / 2;
    this.radius = 0.7;
    this.min = 0;
    this.max = 15;
    this.value = 10;
    this.speed = 5;
    this.startAngle = 0.5 * Math.PI;
    this.endAngle = 1.5 * Math.PI;
    this.progressBgColor = "#FFFFFF21";
    this.progressColor = "#B01F24";
    this._tick = null;
    this._text = null;
    this.lineWidth = 0;
    this.roundCap = true;

    let canvas = (this._canvasElement = document.createElement("canvas"));
    canvas.width = this._w;
    canvas.height = this._h;
    this._ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
    this._bgPattern = null;
    this._imageBgElementCache = null;
    container.appendChild(canvas);
  }

  static init(container: HTMLElement) {
    return new Dashboard(container);
  }

  resize() {
    this._w = parseFloat(getStyle(this.container, "width").replace("px", ""));
    this._h = parseFloat(getStyle(this.container, "height").replace("px", ""));
    this._r = (Math.min(this._w, this._h) / 2) * this.radius;

    this.createImgBgPattern();
    this.drawOnce();
  }

  drawOnce(plain: boolean = false) {
    let ctx = this._ctx;
    let curAngle =
      ((this.value - this.min) * (this.endAngle - this.startAngle)) /
        (this.max - this.min) +
      this.startAngle +
      0.5 * Math.PI;
    curAngle = Math.min(this.endAngle + 0.5 * Math.PI, curAngle);
    ctx.fillStyle = this._bgPattern as any;
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

    this._foregroundCircle(curAngle);
    this.backgroundCircle();
    this.paintTickMark(curAngle);
    if (!plain) {
      this.createText(curAngle);
    }
  }

  /**
   * 选项配置
   * @param options
   */
  setOption(options: IOptions) {
    options.radius && (this.radius = options.radius);
    this._r *= this.radius;
    this.lineWidth = Math.min(this._r / 2, options.lineWidth || this._r / 2);
    defined(options.roundCap) && (this.roundCap = <boolean>options.roundCap);
    defined(options.progressBgImage) &&
      (this.progressBgImage = <string>options.progressBgImage);

    this.startAngle = (((options.startAngle || 0) + 90) / 360) * 2 * Math.PI;
    this.endAngle = (((options.endAngle || 180) + 90) / 360) * 2 * Math.PI;

    defined(options.min) && (this.min = <number>options.min);
    defined(options.max) && (this.max = <number>options.max);
    defined(options.value) && (this.value = <number>options.value);
    defined(options.speed) && (this.speed = <number>options.speed);
    defined(options.progressBgColor) &&
      (this.progressBgColor = <string>options.progressBgColor);
    defined(options.progressColor) &&
      (this.progressColor = <string>options.progressColor);
    defined(options.tick) && (this._tick = <TickOptions>options.tick);
    defined(options.text) && (this._text = <ITextOptions>options.text);
  }

  createImgBgPattern(url?: string) {
    let bgCanvas = document.createElement("canvas");
    let w = (bgCanvas.width = this._w);
    let h = (bgCanvas.height = this._h);
    let bctx = bgCanvas.getContext("2d") as CanvasRenderingContext2D;
    if (url) {
      let img = document.createElement("img");
      img.src = url;
      img.alt = "";
      img.onload = (e: Event) => {
        this._imageBgElementCache = img;
        bctx.drawImage(this._imageBgElementCache, 0, 0, w, h);
        this._bgPattern = this._ctx.createPattern(
          bgCanvas,
          "no-repeat"
        ) as CanvasPattern;
      };
    } else if (this._imageBgElementCache) {
      bctx.drawImage(this._imageBgElementCache, 0, 0, w, h);
      this._bgPattern = this._ctx.createPattern(
        bgCanvas,
        "no-repeat"
      ) as CanvasPattern;
    }
  }

  run() {
    let times = Math.ceil(200 / this.speed);
    let deltaValue = (this.max - this.min) / times;
    let deltaAngle = (this.endAngle - this.startAngle) / times;
    let ctx = this._ctx;
    let curValue = this.min;
    let curAngle = this.startAngle + 0.5 * Math.PI;

    const draw = () => {
      if (curAngle <= this.endAngle + 0.5 * Math.PI) {
        if (this._bgPattern) {
          ctx.fillStyle = this._bgPattern;
          ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
        } else {
          ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
        }
        this._foregroundCircle(curAngle);
        this.backgroundCircle();
        this.paintTickMark(curAngle);
      } else {
        this.drawOnce(true);
      }
      curAngle += deltaAngle;
      curValue += deltaValue;
      this.createText(curValue);
      if (curValue <= this.value) {
        window.requestAnimationFrame(draw);
      }
    };
    draw();
  }

  //#region 私有方法
  private _foregroundCircle(curAngle: number) {
    let ctx = this._ctx;
    let w = this._w;
    let h = this._h;
    let startAngle = this.startAngle + 0.5 * Math.PI;

    ctx.save();
    if (this._bgPattern) {
      ctx.globalCompositeOperation = "destination-in";
    }
    ctx.lineWidth = this.lineWidth;
    ctx.strokeStyle = this.progressColor;
    ctx.lineCap = this.roundCap ? "round" : "square";

    ctx.beginPath();
    ctx.arc(w / 2, h / 2, this._r - 20, startAngle, curAngle, false);
    ctx.stroke();
    ctx.closePath();
    ctx.restore();
  }

  private backgroundCircle() {
    let ctx = this._ctx;
    let W = this._w;
    let H = this._h;
    let startAngle = this.startAngle + 0.5 * Math.PI;
    let endAngle = this.endAngle + 0.5 * Math.PI;
    ctx.save();
    ctx.globalCompositeOperation = "destination-over";
    ctx.lineWidth = this.lineWidth; //设置线宽
    ctx.lineCap = this.roundCap ? "round" : "square";
    ctx.strokeStyle = this.progressBgColor;
    ctx.beginPath();
    ctx.arc(W / 2, H / 2, this._r - 20, startAngle, endAngle, false);
    ctx.stroke();
    ctx.closePath();
    ctx.restore();
  }

  private paintTickMark(curAngle: number) {
    if (!this._tick || !this._tick.isShow) return;
    let tick: TickOptions = this._tick;

    let ctx = this._ctx;
    let W = this._w;
    let H = this._h;

    let delta = 0.001;

    let tickNum: number = tick.tickNum || 8; // 分段个数
    let splitTickNum: number = tick.splitTickNum || 5; // 二级分段个数

    let tickSpanAngle: number = (this.endAngle - this.startAngle) / tickNum; // 每个分段的角度

    let splitTickSpanAngle: number = tickSpanAngle / splitTickNum;
    let splitTickSpanNum: number = Math.floor(splitTickSpanAngle / delta);

    let gradual: boolean = defined(tick.gradual) ? <boolean>tick.gradual : true;

    let strokeAngle = this.startAngle;
    let angleSpan = this.endAngle - this.startAngle;
    let deltaNum = 0;

    let curTickSpan = 0;
    while (strokeAngle <= curAngle - 0.5 * Math.PI) {
      var sin = -Math.sin(strokeAngle);
      var cos = Math.cos(strokeAngle);
      let opacity = (strokeAngle - this.startAngle) / angleSpan;
      if (deltaNum % splitTickSpanNum === 0) {
        ctx.save();
        let lineLength: number = tick.splitTickLength || 10;
        let R = this._r + (tick.splitTickFloat || 15);
        ctx.globalAlpha = gradual ? 1 - opacity : 1;
        ctx.lineWidth = 2;
        ctx.strokeStyle = tick.color || "#444444";
        if (curTickSpan % splitTickNum === 0) {
          // 绘制长线
          lineLength = tick.tickLength || 20;
          R += tick.tickFloat || 0;
        }
        ctx.beginPath();
        ctx.moveTo(
          W / 2 + sin * (R - lineLength / 2),
          H / 2 + cos * (R - lineLength / 2)
        );
        ctx.lineTo(
          W / 2 + sin * (R + lineLength / 2),
          H / 2 + cos * (R + lineLength / 2)
        );
        ctx.stroke();
        ctx.restore();
        curTickSpan++;
      }
      strokeAngle += delta;
      deltaNum++;
    }
  }

  private createText(value: number) {
    if (!this._text || !this._text.isShow) return;
    let text: ITextOptions = this._text;

    let ctx = this._ctx;
    let W = this._w;
    let H = this._h;
    let textStyles = text.styles;

    let lastTextOffsetY = 0;
    text.text.forEach((str: string, index: number) => {
      let style: TextStyle | null = null;
      if (textStyles) {
        style = textStyles[index];
      }
      if (str.includes("{value}")) {
        value = value > this.value ? this.value : value;
        str = str.replace("{value}", value.toFixed(2));
      }
      ctx.save();

      let fontSize = style?.fontSize || 20;
      let marginTop = style?.marginTop || 0;
      let fontWeight = style?.fontWeight || "normal";
      let offsetY = text.offsetY || 0;
      ctx.font = `${fontWeight} ${fontSize}px Helvetica`;
      ctx.fillStyle = style?.color || "#444444";
      ctx.shadowColor = style?.textShadowColor || "rgba(0, 0, 0, 0)";
      ctx.shadowOffsetX = style?.shadowOffsetX || 2;
      ctx.shadowOffsetY = style?.shadowOffsetY || 2;
      ctx.shadowBlur = style?.textShadowBlur || 2;
      var text_width = ctx.measureText(str).width;
      ctx.fillText(
        str,
        W / 2 - text_width / 2,
        H / 2 + offsetY + fontSize / 2 + marginTop + lastTextOffsetY
      );

      lastTextOffsetY += fontSize;
      ctx.restore();
    });
  }
  //#endregion
}

function getStyle(el: any, prop: string) {
  if (window.getComputedStyle) {
    return window.getComputedStyle(el, null).getPropertyValue(prop);
  }
  return el.currentStyle[prop];
}

function defined(value: any) {
  return value !== undefined && value !== null;
}
